/* JSON.schema.ext.walker - A JSONSchema extension traversal module
 *
 * Copyright (c) 2009 Seth Wessitsh (seth@wessitsh.com)
 * Licensed under the MIT (MIT-LICENSE.txt) licence.
 */

var JSON = JSON ? JSON : {};
JSON.schema = JSON.schema ? JSON.schema : {};
JSON.schema.ext = JSON.schema.ext ? JSON.schema.ext : {};

/**
	@todo Prevent circularly referenced schemas from causing infinite recursion

	@interface {
		id: "JSON.schema.ext.walker",
		
		description: "
			An iterator for JSON schema's.  This class will walk the extension hierarchy of the 
			schema provided in a depth first manner.  First returning the initially provided schema,
			then going through the schemas declared in the extends and type restrictions, if any.
			
			Note that the walker defines parent backwards from the extension hierarchy.  I.e.,
			if a schema extends another, the extending schema is the parent of the extended schema.
			E.g., if schema B extends schema A then B is the parent of A.",
		
		parameters: [
			{ name: "schema", type: "JSONSchema", 
				description: "The schema to walk in depth first order." },

			{ name: "path", type: "string", optional: true,
				description: "
					A JSON.path where the `schema' parameter resides.  If none is provided `$' is 
					used." } ],

		methods: {
			walk: {
				description: "Provides an interface for setting the schema to be walked",
				parameters: { "$ref": "$.parameters" } },

			next: {
				description: "Get the next schema in the extension hierarchy, if any.",
				
				returns: { type: ["JSONSchema", "undefined"],
					description: 
						"The next schema in the extension hierarchy, if any, undefined otherwise." } },

			nextBranch: {
				description: "
					Get the schema which occurs first in the next branch of the extension 
					hierarchy.  I.e., the first schema in order with depth less than or equal to the
					depth of the current schema plus the offset parameter.",
				
				parameters: [
					{ name: "offset", type: "integer", default: 0,
						description: "
							A value which will be added to the depth of the current schema when 
							checking if we've reached the depth of the target branch.  This allows
							one to skip multiple branches at once." } ],
				
				returns: { type: ["JSONSchema", "undefined"],
					description: "
						The next schema with depth less than or equal to the depth of the current 
						schema plus the offset parameter, if any, undefined otherwise." } },

			path: {
				description: "
					Get the JSON.path to the current schema using the path passed initially to
					either the constructor or the walk method as the base",
				
				returns: { type: "string",
					description: "The JSON.path to the current schema" } },

			parentPath: {
				description: "
					Get the JSON.path to the parent of the current schema using the path passed
					initially to either the constructor or the walk method as the base", 
				
				returns: { type: "string",
					description: "The JSON.path to the parent of the current schema" } },

			depth: {
				description: "
					Get the depth of the current schema.  Each time we decend into an extends or type
					restriction the depth is increased by 1.  The start depth is 0.  I.e., the depth
					of the schema passed initially to the constructor or the walk method is 0.",
				
				returns: { type: "integer",
					description: "The depth of the current schema" } },

			hasNext: {
				description: "Tells whether there is another schema to be walked or not",
				
				returns: { type: "boolean",
					description: "True if there is another schema to be walked, false otherwise." } },

			branchType: {
				description: "
					Tells whether this schema appeared in the extends restriction or the type 
					restriction of the previous node",

				returns: { type: "string", enum: ["extends", "type"],
					description: "
						`extends' if the current schema appeared in the extends restriction of the 
						parent schema, or `type' if it appeared in the type restriction of the parent 
						schema" } } } }
 */
JSON.schema.ext.walker = function JSONSchemaExtWalker(schema, path) {
	this.cur = undefined;
	this.walk(schema, path);
};

JSON.schema.ext.walker.prototype = {
	walk: function jsewWalk(schema, path) {
		if (schema === undefined) {
			this.toWalk = undefined;
			this.pos = -1;
		} else {
			this.toWalk = [{
				schema: schema, 
				parentPath: undefined,
				path: (path ? path : "$"), 
				depth: 0,
				branchType: "root"
			}];
			this.pos = 0;
		}
	},
	next: function jsewNext() {
		if (this.pos < 0) {
			return;
		}

		this.cur = this.toWalk[this.pos--];

		if (this.cur.schema === undefined || this.cur.schema === null) {
			throw new JSON.schema.err.TypeError(
				"TE000", { current: {
					path: this.cur.path,
					value: this.cur.schema
				}, requires: {value: "[string, object]" } } );
		}

		if (typeof(this.cur.schema) == "string") {
			if (!(this.cur.schema in JSON.schema.registry)) {
				throw new JSON.schema.err.InvalidValueError(
					"IVE000", { current: {
						path: this.cur.path,
						value: this.cur.schema
					} } );
			}

			this.cur.schema = JSON.schema.registry[this.cur.schema];
		}

		this._next(this.cur.schema);

		return this.cur.schema;
	},
	nextBranch: function jsewNextBranch(offset) {
		offset = offset || 0;

		if (this.pos < 0) {
			return;
		}

		var startDepth = this.cur.depth;

		if (this.pos >= 0 && this.toWalk[this.pos].depth > startDepth+offset) { 
			do {
				this.cur = this.toWalk[this.pos--];
			} while (this.pos >= 0 && this.toWalk[this.pos].depth > startDepth+offset);
		}

		return this.pos >= 0 ? undefined : this.cur.schema;
	},
	_next: function jsewGetNext(schema) {
		if (schema === undefined) {
			return;
		}

		switch (Object.typeName(schema.type)) {
			case "string": 
				if (schema === JSON.schema.registry.baseTypes[schema.type]) {
					break;
				}
			// deliberate fall through
			case "object":
				if (schema.type === "" || schema.type === null) {
					break;
				}

				this.toWalk[++this.pos] = {
					schema: schema.type,
					parentPath:  this.cur.path,
					path: 
						this.cur.path + ".type",
					depth: this.cur.depth+1,
					branchType: "type"
				};
			break;
			case "array":
				for (var i=schema.type.length-1; i >=0; i--) {
					this.toWalk[++this.pos] = {
						schema: schema.type[i],
						parentPath:  this.cur.path,
						path: 
							this.cur.path + 
							".type[" + i + "]",
						depth: this.cur.depth+1,
						branchType: "type"
					};
				}
		}

		switch (Object.typeName(schema["extends"])) {
			case "object":
				if (schema["extends"] === null) {
					break;
				}
			// deliberate fall through
			case "string":
				this.toWalk[++this.pos] = {
					schema: schema["extends"],
					parentPath:  this.cur.path,
					path: 
						this.cur.path + 
						".extends",
					depth: this.cur.depth+1,
					branchType: "extends"
				};
		}
	},
	path: function jsewPath() {
		return this.cur ? this.cur.path : "";
	},
	parentPath: function jsewParentPath() {
		return this.cur ? this.cur.parentPath : "";
	},
	depth: function jsewDepth() {
		return this.cur ? this.cur.depth : -1;
	},
	hasNext: function jsewHasNext() {
		return this.pos >= 0;
	},
	branchType: function jsewBranchType() {
		return this.cur ? this.cur.branchType : "";
	}
};